/*
 * Copyright (c) 2000, 2005, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.imageio.plugins.png;

import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageOutputStreamImpl;

class CRC {

  private static int[] crcTable = new int[256];
  private int crc = 0xffffffff;

  static {
    // Initialize CRC table
    for (int n = 0; n < 256; n++) {
      int c = n;
      for (int k = 0; k < 8; k++) {
        if ((c & 1) == 1) {
          c = 0xedb88320 ^ (c >>> 1);
        } else {
          c >>>= 1;
        }

        crcTable[n] = c;
      }
    }
  }

  public CRC() {
  }

  public void reset() {
    crc = 0xffffffff;
  }

  public void update(byte[] data, int off, int len) {
    for (int n = 0; n < len; n++) {
      crc = crcTable[(crc ^ data[off + n]) & 0xff] ^ (crc >>> 8);
    }
  }

  public void update(int data) {
    crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8);
  }

  public int getValue() {
    return crc ^ 0xffffffff;
  }
}


final class ChunkStream extends ImageOutputStreamImpl {

  private ImageOutputStream stream;
  private long startPos;
  private CRC crc = new CRC();

  public ChunkStream(int type, ImageOutputStream stream) throws IOException {
    this.stream = stream;
    this.startPos = stream.getStreamPosition();

    stream.writeInt(-1); // length, will backpatch
    writeInt(type);
  }

  public int read() throws IOException {
    throw new RuntimeException("Method not available");
  }

  public int read(byte[] b, int off, int len) throws IOException {
    throw new RuntimeException("Method not available");
  }

  public void write(byte[] b, int off, int len) throws IOException {
    crc.update(b, off, len);
    stream.write(b, off, len);
  }

  public void write(int b) throws IOException {
    crc.update(b);
    stream.write(b);
  }

  public void finish() throws IOException {
    // Write CRC
    stream.writeInt(crc.getValue());

    // Write length
    long pos = stream.getStreamPosition();
    stream.seek(startPos);
    stream.writeInt((int) (pos - startPos) - 12);

    // Return to end of chunk and flush to minimize buffering
    stream.seek(pos);
    stream.flushBefore(pos);
  }

  protected void finalize() throws Throwable {
    // Empty finalizer (for improved performance; no need to call
    // super.finalize() in this case)
  }
}

// Compress output and write as a series of 'IDAT' chunks of
// fixed length.
final class IDATOutputStream extends ImageOutputStreamImpl {

  private static byte[] chunkType = {
      (byte) 'I', (byte) 'D', (byte) 'A', (byte) 'T'
  };

  private ImageOutputStream stream;
  private int chunkLength;
  private long startPos;
  private CRC crc = new CRC();

  Deflater def = new Deflater(Deflater.BEST_COMPRESSION);
  byte[] buf = new byte[512];

  private int bytesRemaining;

  public IDATOutputStream(ImageOutputStream stream, int chunkLength)
      throws IOException {
    this.stream = stream;
    this.chunkLength = chunkLength;
    startChunk();
  }

  private void startChunk() throws IOException {
    crc.reset();
    this.startPos = stream.getStreamPosition();
    stream.writeInt(-1); // length, will backpatch

    crc.update(chunkType, 0, 4);
    stream.write(chunkType, 0, 4);

    this.bytesRemaining = chunkLength;
  }

  private void finishChunk() throws IOException {
    // Write CRC
    stream.writeInt(crc.getValue());

    // Write length
    long pos = stream.getStreamPosition();
    stream.seek(startPos);
    stream.writeInt((int) (pos - startPos) - 12);

    // Return to end of chunk and flush to minimize buffering
    stream.seek(pos);
    stream.flushBefore(pos);
  }

  public int read() throws IOException {
    throw new RuntimeException("Method not available");
  }

  public int read(byte[] b, int off, int len) throws IOException {
    throw new RuntimeException("Method not available");
  }

  public void write(byte[] b, int off, int len) throws IOException {
    if (len == 0) {
      return;
    }

    if (!def.finished()) {
      def.setInput(b, off, len);
      while (!def.needsInput()) {
        deflate();
      }
    }
  }

  public void deflate() throws IOException {
    int len = def.deflate(buf, 0, buf.length);
    int off = 0;

    while (len > 0) {
      if (bytesRemaining == 0) {
        finishChunk();
        startChunk();
      }

      int nbytes = Math.min(len, bytesRemaining);
      crc.update(buf, off, nbytes);
      stream.write(buf, off, nbytes);

      off += nbytes;
      len -= nbytes;
      bytesRemaining -= nbytes;
    }
  }

  public void write(int b) throws IOException {
    byte[] wbuf = new byte[1];
    wbuf[0] = (byte) b;
    write(wbuf, 0, 1);
  }

  public void finish() throws IOException {
    try {
      if (!def.finished()) {
        def.finish();
        while (!def.finished()) {
          deflate();
        }
      }
      finishChunk();
    } finally {
      def.end();
    }
  }

  protected void finalize() throws Throwable {
    // Empty finalizer (for improved performance; no need to call
    // super.finalize() in this case)
  }
}


class PNGImageWriteParam extends ImageWriteParam {

  public PNGImageWriteParam(Locale locale) {
    super();
    this.canWriteProgressive = true;
    this.locale = locale;
  }
}

/**
 */
public class PNGImageWriter extends ImageWriter {

  ImageOutputStream stream = null;

  PNGMetadata metadata = null;

  // Factors from the ImageWriteParam
  int sourceXOffset = 0;
  int sourceYOffset = 0;
  int sourceWidth = 0;
  int sourceHeight = 0;
  int[] sourceBands = null;
  int periodX = 1;
  int periodY = 1;

  int numBands;
  int bpp;

  RowFilter rowFilter = new RowFilter();
  byte[] prevRow = null;
  byte[] currRow = null;
  byte[][] filteredRows = null;

  // Per-band scaling tables
  //
  // After the first call to initializeScaleTables, either scale and scale0
  // will be valid, or scaleh and scalel will be valid, but not both.
  //
  // The tables will be designed for use with a set of input but depths
  // given by sampleSize, and an output bit depth given by scalingBitDepth.
  //
  int[] sampleSize = null; // Sample size per band, in bits
  int scalingBitDepth = -1; // Output bit depth of the scaling tables

  // Tables for 1, 2, 4, or 8 bit output
  byte[][] scale = null; // 8 bit table
  byte[] scale0 = null; // equivalent to scale[0]

  // Tables for 16 bit output
  byte[][] scaleh = null; // High bytes of output
  byte[][] scalel = null; // Low bytes of output

  int totalPixels; // Total number of pixels to be written by write_IDAT
  int pixelsDone; // Running count of pixels written by write_IDAT

  public PNGImageWriter(ImageWriterSpi originatingProvider) {
    super(originatingProvider);
  }

  public void setOutput(Object output) {
    super.setOutput(output);
    if (output != null) {
      if (!(output instanceof ImageOutputStream)) {
        throw new IllegalArgumentException("output not an ImageOutputStream!");
      }
      this.stream = (ImageOutputStream) output;
    } else {
      this.stream = null;
    }
  }

  private static int[] allowedProgressivePasses = {1, 7};

  public ImageWriteParam getDefaultWriteParam() {
    return new PNGImageWriteParam(getLocale());
  }

  public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
    return null;
  }

  public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
      ImageWriteParam param) {
    PNGMetadata m = new PNGMetadata();
    m.initialize(imageType, imageType.getSampleModel().getNumBands());
    return m;
  }

  public IIOMetadata convertStreamMetadata(IIOMetadata inData,
      ImageWriteParam param) {
    return null;
  }

  public IIOMetadata convertImageMetadata(IIOMetadata inData,
      ImageTypeSpecifier imageType,
      ImageWriteParam param) {
    // TODO - deal with imageType
    if (inData instanceof PNGMetadata) {
      return (PNGMetadata) ((PNGMetadata) inData).clone();
    } else {
      return new PNGMetadata(inData);
    }
  }

  private void write_magic() throws IOException {
    // Write signature
    byte[] magic = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
    stream.write(magic);
  }

  private void write_IHDR() throws IOException {
    // Write IHDR chunk
    ChunkStream cs = new ChunkStream(PNGImageReader.IHDR_TYPE, stream);
    cs.writeInt(metadata.IHDR_width);
    cs.writeInt(metadata.IHDR_height);
    cs.writeByte(metadata.IHDR_bitDepth);
    cs.writeByte(metadata.IHDR_colorType);
    if (metadata.IHDR_compressionMethod != 0) {
      throw new IIOException(
          "Only compression method 0 is defined in PNG 1.1");
    }
    cs.writeByte(metadata.IHDR_compressionMethod);
    if (metadata.IHDR_filterMethod != 0) {
      throw new IIOException(
          "Only filter method 0 is defined in PNG 1.1");
    }
    cs.writeByte(metadata.IHDR_filterMethod);
    if (metadata.IHDR_interlaceMethod < 0 ||
        metadata.IHDR_interlaceMethod > 1) {
      throw new IIOException(
          "Only interlace methods 0 (node) and 1 (adam7) are defined in PNG 1.1");
    }
    cs.writeByte(metadata.IHDR_interlaceMethod);
    cs.finish();
  }

  private void write_cHRM() throws IOException {
    if (metadata.cHRM_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.cHRM_TYPE, stream);
      cs.writeInt(metadata.cHRM_whitePointX);
      cs.writeInt(metadata.cHRM_whitePointY);
      cs.writeInt(metadata.cHRM_redX);
      cs.writeInt(metadata.cHRM_redY);
      cs.writeInt(metadata.cHRM_greenX);
      cs.writeInt(metadata.cHRM_greenY);
      cs.writeInt(metadata.cHRM_blueX);
      cs.writeInt(metadata.cHRM_blueY);
      cs.finish();
    }
  }

  private void write_gAMA() throws IOException {
    if (metadata.gAMA_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.gAMA_TYPE, stream);
      cs.writeInt(metadata.gAMA_gamma);
      cs.finish();
    }
  }

  private void write_iCCP() throws IOException {
    if (metadata.iCCP_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream);
      cs.writeBytes(metadata.iCCP_profileName);
      cs.writeByte(0); // null terminator

      cs.writeByte(metadata.iCCP_compressionMethod);
      cs.write(metadata.iCCP_compressedProfile);
      cs.finish();
    }
  }

  private void write_sBIT() throws IOException {
    if (metadata.sBIT_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream);
      int colorType = metadata.IHDR_colorType;
      if (metadata.sBIT_colorType != colorType) {
        processWarningOccurred(0,
            "sBIT metadata has wrong color type.\n" +
                "The chunk will not be written.");
        return;
      }

      if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
          colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
        cs.writeByte(metadata.sBIT_grayBits);
      } else if (colorType == PNGImageReader.PNG_COLOR_RGB ||
          colorType == PNGImageReader.PNG_COLOR_PALETTE ||
          colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
        cs.writeByte(metadata.sBIT_redBits);
        cs.writeByte(metadata.sBIT_greenBits);
        cs.writeByte(metadata.sBIT_blueBits);
      }

      if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
          colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
        cs.writeByte(metadata.sBIT_alphaBits);
      }
      cs.finish();
    }
  }

  private void write_sRGB() throws IOException {
    if (metadata.sRGB_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream);
      cs.writeByte(metadata.sRGB_renderingIntent);
      cs.finish();
    }
  }

  private void write_PLTE() throws IOException {
    if (metadata.PLTE_present) {
      if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY ||
          metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
        // PLTE cannot occur in a gray image

        processWarningOccurred(0,
            "A PLTE chunk may not appear in a gray or gray alpha image.\n" +
                "The chunk will not be written");
        return;
      }

      ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream);

      int numEntries = metadata.PLTE_red.length;
      byte[] palette = new byte[numEntries * 3];
      int index = 0;
      for (int i = 0; i < numEntries; i++) {
        palette[index++] = metadata.PLTE_red[i];
        palette[index++] = metadata.PLTE_green[i];
        palette[index++] = metadata.PLTE_blue[i];
      }

      cs.write(palette);
      cs.finish();
    }
  }

  private void write_hIST() throws IOException, IIOException {
    if (metadata.hIST_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream);

      if (!metadata.PLTE_present) {
        throw new IIOException("hIST chunk without PLTE chunk!");
      }

      cs.writeChars(metadata.hIST_histogram,
          0, metadata.hIST_histogram.length);
      cs.finish();
    }
  }

  private void write_tRNS() throws IOException, IIOException {
    if (metadata.tRNS_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream);
      int colorType = metadata.IHDR_colorType;
      int chunkType = metadata.tRNS_colorType;

      // Special case: image is RGB and chunk is Gray
      // Promote chunk contents to RGB
      int chunkRed = metadata.tRNS_red;
      int chunkGreen = metadata.tRNS_green;
      int chunkBlue = metadata.tRNS_blue;
      if (colorType == PNGImageReader.PNG_COLOR_RGB &&
          chunkType == PNGImageReader.PNG_COLOR_GRAY) {
        chunkType = colorType;
        chunkRed = chunkGreen = chunkBlue =
            metadata.tRNS_gray;
      }

      if (chunkType != colorType) {
        processWarningOccurred(0,
            "tRNS metadata has incompatible color type.\n" +
                "The chunk will not be written.");
        return;
      }

      if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
        if (!metadata.PLTE_present) {
          throw new IIOException("tRNS chunk without PLTE chunk!");
        }
        cs.write(metadata.tRNS_alpha);
      } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) {
        cs.writeShort(metadata.tRNS_gray);
      } else if (colorType == PNGImageReader.PNG_COLOR_RGB) {
        cs.writeShort(chunkRed);
        cs.writeShort(chunkGreen);
        cs.writeShort(chunkBlue);
      } else {
        throw new IIOException("tRNS chunk for color type 4 or 6!");
      }
      cs.finish();
    }
  }

  private void write_bKGD() throws IOException {
    if (metadata.bKGD_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream);
      int colorType = metadata.IHDR_colorType & 0x3;
      int chunkType = metadata.bKGD_colorType;

      // Special case: image is RGB(A) and chunk is Gray
      // Promote chunk contents to RGB
      int chunkRed = metadata.bKGD_red;
      int chunkGreen = metadata.bKGD_red;
      int chunkBlue = metadata.bKGD_red;
      if (colorType == PNGImageReader.PNG_COLOR_RGB &&
          chunkType == PNGImageReader.PNG_COLOR_GRAY) {
        // Make a gray bKGD chunk look like RGB
        chunkType = colorType;
        chunkRed = chunkGreen = chunkBlue =
            metadata.bKGD_gray;
      }

      // Ignore status of alpha in colorType
      if (chunkType != colorType) {
        processWarningOccurred(0,
            "bKGD metadata has incompatible color type.\n" +
                "The chunk will not be written.");
        return;
      }

      if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
        cs.writeByte(metadata.bKGD_index);
      } else if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
          colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
        cs.writeShort(metadata.bKGD_gray);
      } else { // colorType == PNGImageReader.PNG_COLOR_RGB ||
        // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
        cs.writeShort(chunkRed);
        cs.writeShort(chunkGreen);
        cs.writeShort(chunkBlue);
      }
      cs.finish();
    }
  }

  private void write_pHYs() throws IOException {
    if (metadata.pHYs_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream);
      cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis);
      cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis);
      cs.writeByte(metadata.pHYs_unitSpecifier);
      cs.finish();
    }
  }

  private void write_sPLT() throws IOException {
    if (metadata.sPLT_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream);

      cs.writeBytes(metadata.sPLT_paletteName);
      cs.writeByte(0); // null terminator

      cs.writeByte(metadata.sPLT_sampleDepth);
      int numEntries = metadata.sPLT_red.length;

      if (metadata.sPLT_sampleDepth == 8) {
        for (int i = 0; i < numEntries; i++) {
          cs.writeByte(metadata.sPLT_red[i]);
          cs.writeByte(metadata.sPLT_green[i]);
          cs.writeByte(metadata.sPLT_blue[i]);
          cs.writeByte(metadata.sPLT_alpha[i]);
          cs.writeShort(metadata.sPLT_frequency[i]);
        }
      } else { // sampleDepth == 16
        for (int i = 0; i < numEntries; i++) {
          cs.writeShort(metadata.sPLT_red[i]);
          cs.writeShort(metadata.sPLT_green[i]);
          cs.writeShort(metadata.sPLT_blue[i]);
          cs.writeShort(metadata.sPLT_alpha[i]);
          cs.writeShort(metadata.sPLT_frequency[i]);
        }
      }
      cs.finish();
    }
  }

  private void write_tIME() throws IOException {
    if (metadata.tIME_present) {
      ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream);
      cs.writeShort(metadata.tIME_year);
      cs.writeByte(metadata.tIME_month);
      cs.writeByte(metadata.tIME_day);
      cs.writeByte(metadata.tIME_hour);
      cs.writeByte(metadata.tIME_minute);
      cs.writeByte(metadata.tIME_second);
      cs.finish();
    }
  }

  private void write_tEXt() throws IOException {
    Iterator keywordIter = metadata.tEXt_keyword.iterator();
    Iterator textIter = metadata.tEXt_text.iterator();

    while (keywordIter.hasNext()) {
      ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream);
      String keyword = (String) keywordIter.next();
      cs.writeBytes(keyword);
      cs.writeByte(0);

      String text = (String) textIter.next();
      cs.writeBytes(text);
      cs.finish();
    }
  }

  private byte[] deflate(byte[] b) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    DeflaterOutputStream dos = new DeflaterOutputStream(baos);
    dos.write(b);
    dos.close();
    return baos.toByteArray();
  }

  private void write_iTXt() throws IOException {
    Iterator<String> keywordIter = metadata.iTXt_keyword.iterator();
    Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator();
    Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator();
    Iterator<String> languageIter = metadata.iTXt_languageTag.iterator();
    Iterator<String> translatedKeywordIter =
        metadata.iTXt_translatedKeyword.iterator();
    Iterator<String> textIter = metadata.iTXt_text.iterator();

    while (keywordIter.hasNext()) {
      ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream);

      cs.writeBytes(keywordIter.next());
      cs.writeByte(0);

      Boolean compressed = flagIter.next();
      cs.writeByte(compressed ? 1 : 0);

      cs.writeByte(methodIter.next().intValue());

      cs.writeBytes(languageIter.next());
      cs.writeByte(0);

      cs.write(translatedKeywordIter.next().getBytes("UTF8"));
      cs.writeByte(0);

      String text = textIter.next();
      if (compressed) {
        cs.write(deflate(text.getBytes("UTF8")));
      } else {
        cs.write(text.getBytes("UTF8"));
      }
      cs.finish();
    }
  }

  private void write_zTXt() throws IOException {
    Iterator keywordIter = metadata.zTXt_keyword.iterator();
    Iterator methodIter = metadata.zTXt_compressionMethod.iterator();
    Iterator textIter = metadata.zTXt_text.iterator();

    while (keywordIter.hasNext()) {
      ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream);
      String keyword = (String) keywordIter.next();
      cs.writeBytes(keyword);
      cs.writeByte(0);

      int compressionMethod = ((Integer) methodIter.next()).intValue();
      cs.writeByte(compressionMethod);

      String text = (String) textIter.next();
      cs.write(deflate(text.getBytes("ISO-8859-1")));
      cs.finish();
    }
  }

  private void writeUnknownChunks() throws IOException {
    Iterator typeIter = metadata.unknownChunkType.iterator();
    Iterator dataIter = metadata.unknownChunkData.iterator();

    while (typeIter.hasNext() && dataIter.hasNext()) {
      String type = (String) typeIter.next();
      ChunkStream cs = new ChunkStream(chunkType(type), stream);
      byte[] data = (byte[]) dataIter.next();
      cs.write(data);
      cs.finish();
    }
  }

  private static int chunkType(String typeString) {
    char c0 = typeString.charAt(0);
    char c1 = typeString.charAt(1);
    char c2 = typeString.charAt(2);
    char c3 = typeString.charAt(3);

    int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3;
    return type;
  }

  private void encodePass(ImageOutputStream os,
      RenderedImage image,
      int xOffset, int yOffset,
      int xSkip, int ySkip) throws IOException {
    int minX = sourceXOffset;
    int minY = sourceYOffset;
    int width = sourceWidth;
    int height = sourceHeight;

    // Adjust offsets and skips based on source subsampling factors
    xOffset *= periodX;
    xSkip *= periodX;
    yOffset *= periodY;
    ySkip *= periodY;

    // Early exit if no data for this pass
    int hpixels = (width - xOffset + xSkip - 1) / xSkip;
    int vpixels = (height - yOffset + ySkip - 1) / ySkip;
    if (hpixels == 0 || vpixels == 0) {
      return;
    }

    // Convert X offset and skip from pixels to samples
    xOffset *= numBands;
    xSkip *= numBands;

    // Create row buffers
    int samplesPerByte = 8 / metadata.IHDR_bitDepth;
    int numSamples = width * numBands;
    int[] samples = new int[numSamples];

    int bytesPerRow = hpixels * numBands;
    if (metadata.IHDR_bitDepth < 8) {
      bytesPerRow = (bytesPerRow + samplesPerByte - 1) / samplesPerByte;
    } else if (metadata.IHDR_bitDepth == 16) {
      bytesPerRow *= 2;
    }

    IndexColorModel icm_gray_alpha = null;
    if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA &&
        image.getColorModel() instanceof IndexColorModel) {
      // reserve space for alpha samples
      bytesPerRow *= 2;

      // will be used to calculate alpha value for the pixel
      icm_gray_alpha = (IndexColorModel) image.getColorModel();
    }

    currRow = new byte[bytesPerRow + bpp];
    prevRow = new byte[bytesPerRow + bpp];
    filteredRows = new byte[5][bytesPerRow + bpp];

    int bitDepth = metadata.IHDR_bitDepth;
    for (int row = minY + yOffset; row < minY + height; row += ySkip) {
      Rectangle rect = new Rectangle(minX, row, width, 1);
      Raster ras = image.getData(rect);
      if (sourceBands != null) {
        ras = ras.createChild(minX, row, width, 1, minX, row,
            sourceBands);
      }

      ras.getPixels(minX, row, width, 1, samples);

      if (image.getColorModel().isAlphaPremultiplied()) {
        WritableRaster wr = ras.createCompatibleWritableRaster();
        wr.setPixels(wr.getMinX(), wr.getMinY(),
            wr.getWidth(), wr.getHeight(),
            samples);

        image.getColorModel().coerceData(wr, false);
        wr.getPixels(wr.getMinX(), wr.getMinY(),
            wr.getWidth(), wr.getHeight(),
            samples);
      }

      // Reorder palette data if necessary
      int[] paletteOrder = metadata.PLTE_order;
      if (paletteOrder != null) {
        for (int i = 0; i < numSamples; i++) {
          samples[i] = paletteOrder[samples[i]];
        }
      }

      int count = bpp; // leave first 'bpp' bytes zero
      int pos = 0;
      int tmp = 0;

      switch (bitDepth) {
        case 1:
        case 2:
        case 4:
          // Image can only have a single band

          int mask = samplesPerByte - 1;
          for (int s = xOffset; s < numSamples; s += xSkip) {
            byte val = scale0[samples[s]];
            tmp = (tmp << bitDepth) | val;

            if ((pos++ & mask) == mask) {
              currRow[count++] = (byte) tmp;
              tmp = 0;
              pos = 0;
            }
          }

          // Left shift the last byte
          if ((pos & mask) != 0) {
            tmp <<= ((8 / bitDepth) - pos) * bitDepth;
            currRow[count++] = (byte) tmp;
          }
          break;

        case 8:
          if (numBands == 1) {
            for (int s = xOffset; s < numSamples; s += xSkip) {
              currRow[count++] = scale0[samples[s]];
              if (icm_gray_alpha != null) {
                currRow[count++] =
                    scale0[icm_gray_alpha.getAlpha(0xff & samples[s])];
              }
            }
          } else {
            for (int s = xOffset; s < numSamples; s += xSkip) {
              for (int b = 0; b < numBands; b++) {
                currRow[count++] = scale[b][samples[s + b]];
              }
            }
          }
          break;

        case 16:
          for (int s = xOffset; s < numSamples; s += xSkip) {
            for (int b = 0; b < numBands; b++) {
              currRow[count++] = scaleh[b][samples[s + b]];
              currRow[count++] = scalel[b][samples[s + b]];
            }
          }
          break;
      }

      // Perform filtering
      int filterType = rowFilter.filterRow(metadata.IHDR_colorType,
          currRow, prevRow,
          filteredRows,
          bytesPerRow, bpp);

      os.write(filterType);
      os.write(filteredRows[filterType], bpp, bytesPerRow);

      // Swap current and previous rows
      byte[] swap = currRow;
      currRow = prevRow;
      prevRow = swap;

      pixelsDone += hpixels;
      processImageProgress(100.0F * pixelsDone / totalPixels);

      // If write has been aborted, just return;
      // processWriteAborted will be called later
      if (abortRequested()) {
        return;
      }
    }
  }

  // Use sourceXOffset, etc.
  private void write_IDAT(RenderedImage image) throws IOException {
    IDATOutputStream ios = new IDATOutputStream(stream, 32768);
    try {
      if (metadata.IHDR_interlaceMethod == 1) {
        for (int i = 0; i < 7; i++) {
          encodePass(ios, image,
              PNGImageReader.adam7XOffset[i],
              PNGImageReader.adam7YOffset[i],
              PNGImageReader.adam7XSubsampling[i],
              PNGImageReader.adam7YSubsampling[i]);
          if (abortRequested()) {
            break;
          }
        }
      } else {
        encodePass(ios, image, 0, 0, 1, 1);
      }
    } finally {
      ios.finish();
    }
  }

  private void writeIEND() throws IOException {
    ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream);
    cs.finish();
  }

  // Check two int arrays for value equality, always returns false
  // if either array is null
  private boolean equals(int[] s0, int[] s1) {
    if (s0 == null || s1 == null) {
      return false;
    }
    if (s0.length != s1.length) {
      return false;
    }
    for (int i = 0; i < s0.length; i++) {
      if (s0[i] != s1[i]) {
        return false;
      }
    }
    return true;
  }

  // Initialize the scale/scale0 or scaleh/scalel arrays to
  // hold the results of scaling an input value to the desired
  // output bit depth
  private void initializeScaleTables(int[] sampleSize) {
    int bitDepth = metadata.IHDR_bitDepth;

    // If the existing tables are still valid, just return
    if (bitDepth == scalingBitDepth &&
        equals(sampleSize, this.sampleSize)) {
      return;
    }

    // Compute new tables
    this.sampleSize = sampleSize;
    this.scalingBitDepth = bitDepth;
    int maxOutSample = (1 << bitDepth) - 1;
    if (bitDepth <= 8) {
      scale = new byte[numBands][];
      for (int b = 0; b < numBands; b++) {
        int maxInSample = (1 << sampleSize[b]) - 1;
        int halfMaxInSample = maxInSample / 2;
        scale[b] = new byte[maxInSample + 1];
        for (int s = 0; s <= maxInSample; s++) {
          scale[b][s] =
              (byte) ((s * maxOutSample + halfMaxInSample) / maxInSample);
        }
      }
      scale0 = scale[0];
      scaleh = scalel = null;
    } else { // bitDepth == 16
      // Divide scaling table into high and low bytes
      scaleh = new byte[numBands][];
      scalel = new byte[numBands][];

      for (int b = 0; b < numBands; b++) {
        int maxInSample = (1 << sampleSize[b]) - 1;
        int halfMaxInSample = maxInSample / 2;
        scaleh[b] = new byte[maxInSample + 1];
        scalel[b] = new byte[maxInSample + 1];
        for (int s = 0; s <= maxInSample; s++) {
          int val = (s * maxOutSample + halfMaxInSample) / maxInSample;
          scaleh[b][s] = (byte) (val >> 8);
          scalel[b][s] = (byte) (val & 0xff);
        }
      }
      scale = null;
      scale0 = null;
    }
  }

  public void write(IIOMetadata streamMetadata,
      IIOImage image,
      ImageWriteParam param) throws IIOException {
    if (stream == null) {
      throw new IllegalStateException("output == null!");
    }
    if (image == null) {
      throw new IllegalArgumentException("image == null!");
    }
    if (image.hasRaster()) {
      throw new UnsupportedOperationException("image has a Raster!");
    }

    RenderedImage im = image.getRenderedImage();
    SampleModel sampleModel = im.getSampleModel();
    this.numBands = sampleModel.getNumBands();

    // Set source region and subsampling to default values
    this.sourceXOffset = im.getMinX();
    this.sourceYOffset = im.getMinY();
    this.sourceWidth = im.getWidth();
    this.sourceHeight = im.getHeight();
    this.sourceBands = null;
    this.periodX = 1;
    this.periodY = 1;

    if (param != null) {
      // Get source region and subsampling factors
      Rectangle sourceRegion = param.getSourceRegion();
      if (sourceRegion != null) {
        Rectangle imageBounds = new Rectangle(im.getMinX(),
            im.getMinY(),
            im.getWidth(),
            im.getHeight());
        // Clip to actual image bounds
        sourceRegion = sourceRegion.intersection(imageBounds);
        sourceXOffset = sourceRegion.x;
        sourceYOffset = sourceRegion.y;
        sourceWidth = sourceRegion.width;
        sourceHeight = sourceRegion.height;
      }

      // Adjust for subsampling offsets
      int gridX = param.getSubsamplingXOffset();
      int gridY = param.getSubsamplingYOffset();
      sourceXOffset += gridX;
      sourceYOffset += gridY;
      sourceWidth -= gridX;
      sourceHeight -= gridY;

      // Get subsampling factors
      periodX = param.getSourceXSubsampling();
      periodY = param.getSourceYSubsampling();

      int[] sBands = param.getSourceBands();
      if (sBands != null) {
        sourceBands = sBands;
        numBands = sourceBands.length;
      }
    }

    // Compute output dimensions
    int destWidth = (sourceWidth + periodX - 1) / periodX;
    int destHeight = (sourceHeight + periodY - 1) / periodY;
    if (destWidth <= 0 || destHeight <= 0) {
      throw new IllegalArgumentException("Empty source region!");
    }

    // Compute total number of pixels for progress notification
    this.totalPixels = destWidth * destHeight;
    this.pixelsDone = 0;

    // Create metadata
    IIOMetadata imd = image.getMetadata();
    if (imd != null) {
      metadata = (PNGMetadata) convertImageMetadata(imd,
          ImageTypeSpecifier.createFromRenderedImage(im),
          null);
    } else {
      metadata = new PNGMetadata();
    }

    if (param != null) {
      // Use Adam7 interlacing if set in write param
      switch (param.getProgressiveMode()) {
        case ImageWriteParam.MODE_DEFAULT:
          metadata.IHDR_interlaceMethod = 1;
          break;
        case ImageWriteParam.MODE_DISABLED:
          metadata.IHDR_interlaceMethod = 0;
          break;
        // MODE_COPY_FROM_METADATA should alreay be taken care of
        // MODE_EXPLICIT is not allowed
      }
    }

    // Initialize bitDepth and colorType
    metadata.initialize(new ImageTypeSpecifier(im), numBands);

    // Overwrite IHDR width and height values with values from image
    metadata.IHDR_width = destWidth;
    metadata.IHDR_height = destHeight;

    this.bpp = numBands * ((metadata.IHDR_bitDepth == 16) ? 2 : 1);

    // Initialize scaling tables for this image
    initializeScaleTables(sampleModel.getSampleSize());

    clearAbortRequest();

    processImageStarted(0);

    try {
      write_magic();
      write_IHDR();

      write_cHRM();
      write_gAMA();
      write_iCCP();
      write_sBIT();
      write_sRGB();

      write_PLTE();

      write_hIST();
      write_tRNS();
      write_bKGD();

      write_pHYs();
      write_sPLT();
      write_tIME();
      write_tEXt();
      write_iTXt();
      write_zTXt();

      writeUnknownChunks();

      write_IDAT(im);

      if (abortRequested()) {
        processWriteAborted();
      } else {
        // Finish up and inform the listeners we are done
        writeIEND();
        processImageComplete();
      }
    } catch (IOException e) {
      throw new IIOException("I/O error writing PNG file!", e);
    }
  }
}
